package top.lishuoboy.shiro02_springboot_jsp.config;

import at.pollux.thymeleaf.shiro.dialect.ShiroDialect;
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.serializer.SerializerFeature;
import org.apache.shiro.authc.credential.HashedCredentialsMatcher;
import org.apache.shiro.mgt.SecurityManager;
import org.apache.shiro.realm.Realm;
import org.apache.shiro.spring.web.ShiroFilterFactoryBean;
import org.apache.shiro.web.mgt.DefaultWebSecurityManager;
import org.apache.shiro.web.session.mgt.DefaultWebSessionManager;
import org.crazycake.shiro.RedisCacheManager;
import org.crazycake.shiro.RedisManager;
import org.crazycake.shiro.RedisSessionDAO;
import org.crazycake.shiro.serializer.RedisSerializer;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import top.lishuoboy.shiro02_springboot_jsp.shiro.realm.CustomizeMd5Realm;
import top.lishuoboy.shiro02_springboot_jsp.shiro.realm.CustomizeMd5RealmTel;

import java.util.Arrays;
import java.util.LinkedHashMap;
import java.util.Map;

/**
 * @author lishuoboy
 * @date 2022/2/16 19:16
 */
@Configuration
public class ShiroConfig {

    @Bean
    public ShiroFilterFactoryBean shiroFilterFactoryBean(SecurityManager securityManager) {
        //创建shiro的filter
        ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
        //注入安全管理器
        shiroFilterFactoryBean.setSecurityManager(securityManager);

        // 6.4 常见过滤器
        // shiro提供和多个默认的过滤器，我们可以用这些过滤器来配置控制指定url的权限：
        // 缩写	             对应的过滤器	                    功能
        // anon	             AnonymousFilter	            指定url可以匿名访问
        // authc	         FormAuthenticationFilter	    指定url需要form表单登录，默认会从请求中获取username、password,rememberMe等参数并尝试登录，如果登录不了就会跳转到loginUrl配置的路径。我们也可以用这个过滤器做默认的登录逻辑，但是一般都是我们自己在控制器写登录逻辑的，自己写的话出错返回的信息都可以定制嘛。
        // authcBasic	     BasicHttpAuthenticationFilter	指定url需要basic登录
        // logout	         LogoutFilter	                登出过滤器，配置指定url就可以实现退出功能，非常方便
        // noSessionCreation NoSessionCreationFilter	    禁止创建会话
        // perms	         PermissionsAuthorizationFilter	需要指定权限才能访问
        // port	             PortFilter	                    需要指定端口才能访问
        // rest	             HttpMethodPermissionFilter	    将http请求方法转化成相应的动词来构造一个权限字符串，这个感觉意义不大，有兴趣自己看源码的注释
        // roles	         RolesAuthorizationFilter	    需要指定角色才能访问
        // ssl	             SslFilter	                    需要https请求才能访问
        // user	             UserFilter	                    需要已登录或“记住我”的用户才能访问

        // 配置过滤器，默认在配置好shiro环境后默认环境中没有对项目中任何资源进行权限控制,所有现在项目中所有资源都可以通过路径访问
        Map<String, String> filterMap = new LinkedHashMap();
        // anon  AnonymousFilter [əˈnɑːnɪməs] 指定url可以匿名访问
        filterMap.put("/hello_thymeleaf", "anon");
        filterMap.put("/hello_mp", "anon");
        filterMap.put("/", "anon");
        filterMap.put("/index", "anon");
        filterMap.put("/login", "anon");
        filterMap.put("/doLogin", "anon");
        filterMap.put("/register", "anon");
        filterMap.put("/doRegister", "anon");
        filterMap.put("/doCheckUserNameIsExists", "anon");
        filterMap.put("/testPermissionByAnnotation", "anon");

        filterMap.put("/html/**", "anon");
        filterMap.put("/js/**", "anon");
        filterMap.put("/img/**", "anon");
        filterMap.put("/webjars/**", "anon");

        filterMap.put("/getCaptcha", "anon");
        // authc FormAuthenticationFilter [ɔːˌθentɪˈkeɪʃn]	指定url需要form表单登录，默认会从请求中获取username、password,rememberMe等参数并尝试登录，如果登录不了就会跳转到loginUrl配置的路径。我们也可以用这个过滤器做默认的登录逻辑，但是一般都是我们自己在控制器写登录逻辑的，自己写的话出错返回的信息都可以定制嘛。
        filterMap.put("/**", "authc");
        shiroFilterFactoryBean.setFilterChainDefinitionMap(filterMap);

        // 设置拦截未认证请求后跳转的loginUrl，默认为login.jsp(在配置文件中不生效)，用代码配置
        shiroFilterFactoryBean.setLoginUrl("/login");

        return shiroFilterFactoryBean;
    }

    @Bean
    public DefaultWebSecurityManager defaultWebSecurityManager(Realm realm, Realm realmTel) {
        DefaultWebSecurityManager defaultWebSecurityManager = new DefaultWebSecurityManager();
        /**  3、shiro 将 session 共享在 redis */
        defaultWebSecurityManager.setSessionManager(defaultWebSessionManager());
        // 配置多个realm。导致认证时抛出的异常不细，不抛UnknownAccountException(用户名不存在)、IncorrectCredentialsException(密码错误)，直接抛AuthenticationException（认证异常）
        defaultWebSecurityManager.setRealms(Arrays.asList(realm, realmTel));
        return defaultWebSecurityManager;
    }

    /** 2、shiro 将 session 共享在 redis */
    @Bean
    DefaultWebSessionManager defaultWebSessionManager() {
        DefaultWebSessionManager defaultWebSessionManager = new DefaultWebSessionManager();
        defaultWebSessionManager.setSessionDAO(redisSessionDAO());
        return defaultWebSessionManager;
    }

    /** 1、shiro 将 session 共享在 redis */
    @Bean
    RedisSessionDAO redisSessionDAO() {
        RedisSessionDAO redisSessionDAO = new RedisSessionDAO();
        redisSessionDAO.setRedisManager(redisManager());
//        redisSessionDAO.setValueSerializer(jsonRedisSerializer());    // session json序列化，失败，丢失数据
        /* 修改 session 有效期，默认-2
            若 shiro 不使用 redis 存储session，则以此 subject的时间（默认1800秒）为准。即subject.getSession().setTimeout(1800_000);
            若 shiro  启用 redis 存储session，优先以 redis 设置的有效期（redisSessionDAO.setExpire(1800);）为准。
        */
        redisSessionDAO.setExpire(1800);  // shiro 写入 redis 的 session 的有效期，默认30分，即 1800 秒
        return redisSessionDAO;
    }

    @Bean
    public Realm realm(RedisCacheManager redisCacheManager) {
        //设置realm使用hash凭证匹配器
        HashedCredentialsMatcher credentialsMatcher = new HashedCredentialsMatcher();
        //使用算法
        credentialsMatcher.setHashAlgorithmName("MD5");
        //散列次数
        credentialsMatcher.setHashIterations(1024);
        CustomizeMd5Realm customizeMd5Realm = new CustomizeMd5Realm();
        customizeMd5Realm.setCredentialsMatcher(credentialsMatcher);

        //开启缓存管理
//        customizeMd5Realm.setCacheManager(new MemoryConstrainedCacheManager());  // 1、使用shiro内置缓存管理器
//        customizeMd5Realm.setCacheManager(new EhCacheManager());                 // 2、使用EhCache 缓存管理器
        customizeMd5Realm.setCacheManager(redisCacheManager);                      // 3、使用Redis   缓存管理器
        customizeMd5Realm.setCachingEnabled(true);  //开启全局缓存
        // 开启认证缓存，开启后，subject.login(token) 只调用一次 CustomizeMd5Realm.doGetAuthenticationInfo()，之后不再重复调用
        customizeMd5Realm.setAuthenticationCachingEnabled(true);
        customizeMd5Realm.setAuthenticationCacheName("authenticationCache");
        // 开启授权缓存，开启后，subject.hasRole()、subject.isPermitted、@RequiresRoles、@RequiresPermissions等 任意一个方法第一次执行会调用 CustomizeMd5Realm.doGetAuthenticationInfo()，之后不再重复调用
        customizeMd5Realm.setAuthorizationCachingEnabled(true);
        customizeMd5Realm.setAuthorizationCacheName("authorizationCache");

        return customizeMd5Realm;
    }

    @Bean
    public Realm realmTel(RedisCacheManager redisCacheManager) {
        //设置realm使用hash凭证匹配器
        HashedCredentialsMatcher credentialsMatcher = new HashedCredentialsMatcher();
        //使用算法
        credentialsMatcher.setHashAlgorithmName("MD5");
        //散列次数
        credentialsMatcher.setHashIterations(1024);
        CustomizeMd5RealmTel customizeMd5RealmTel = new CustomizeMd5RealmTel();
        customizeMd5RealmTel.setCredentialsMatcher(credentialsMatcher);

        //开启缓存管理
//        customizeMd5RealmTel.setCacheManager(new MemoryConstrainedCacheManager());  // 1、使用shiro内置缓存管理器
//        customizeMd5RealmTel.setCacheManager(new EhCacheManager());                 // 2、使用EhCache 缓存管理器
        customizeMd5RealmTel.setCacheManager(redisCacheManager);                      // 3、使用Redis   缓存管理器
        customizeMd5RealmTel.setCachingEnabled(true);  //开启全局缓存
        // 开启认证缓存，开启后，subject.login(token) 只调用一次 CustomizeMd5Realm.doGetAuthenticationInfo()，之后不再重复调用
        customizeMd5RealmTel.setAuthenticationCachingEnabled(true);
        customizeMd5RealmTel.setAuthenticationCacheName("authenticationCache");
        // 开启授权缓存，开启后，subject.hasRole()、subject.isPermitted、@RequiresRoles、@RequiresPermissions等 任意一个方法第一次执行会调用 CustomizeMd5Realm.doGetAuthenticationInfo()，之后不再重复调用
        customizeMd5RealmTel.setAuthorizationCachingEnabled(true);
        customizeMd5RealmTel.setAuthorizationCacheName("authorizationCache");

        return customizeMd5RealmTel;
    }

    @Bean
    public RedisCacheManager redisCacheManager() {
        RedisCacheManager redisCacheManager = new RedisCacheManager();
        redisCacheManager.setRedisManager(redisManager());
        redisCacheManager.setValueSerializer(jsonRedisSerializer());  // 默认为jdk序列化，改为json
        // shiro 写入 redis 的 key（认证和授权缓存数据） 的有效期，默认30分，即 1800 秒
        redisCacheManager.setExpire(180);
        return redisCacheManager;
    }

    @Bean
    RedisManager redisManager() {
        // 配置 redis 的 IP、端口、密码等信息
        RedisManager redisManager = new RedisManager();
//        redisManager.setJedisPool();
//        redisManager.setHost("127.0.0.1:6379");
//        redisManager.setPassword(null);
//        redisManager.setTimeout(2000);
        return redisManager;
    }

    /** shiro权限标签不起作用一定要记住加入方言处理 */
    @Bean(name = "shiroDialect")
    public ShiroDialect shiroDialect() {
        return new ShiroDialect();
    }

    /** shiro redis json 序列化器 */
    @Bean
    RedisSerializer jsonRedisSerializer() {
        RedisSerializer<Object> jsonRedisSerializer = new RedisSerializer<>() {
            @Override
            public byte[] serialize(Object obj) {
                if (obj == null) {
                    return new byte[0];
                }

                // hutool
//                Map<String, Object> objMap = HuJsonUtil.toMap(HuJsonUtil.toJsonStr(obj));
//                String type = obj.getClass().toString().split(" ")[1];
//                objMap.put("@type",type);
//                return HuJsonUtil.toJsonPrettyStr(objMap).getBytes(StandardCharsets.UTF_8);

                // fastjson
                return JSON.toJSONBytes(obj, SerializerFeature.WriteClassName);
            }

            @Override
            public Object deserialize(byte[] bytes) {
                if (bytes == null || bytes.length == 0) {
                    return null;
                }

                // hutool
//                Map objMap = HuJsonUtil.toMap(new String(bytes, StandardCharsets.UTF_8));
//                Class objClass = Class.forName((String) objMap.get("@type")).newInstance().getClass();  // 反射获取实例
//                return HuJsonUtil.toBean(HuJsonUtil.toJsonStr(objMap, JSONConfig.create().setIgnoreError(true).setNatureKeyComparator(), false), objClass);

                // fastjson
                return JSON.parse(bytes);
            }
        };
        return jsonRedisSerializer;
    }
}
